package ninetails;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import jnausb.DeviceDescriptor;
import jnausb.LibUSB;
import jnausb.LibUSB.Context;
import jnausb.LibUSB.Device;
import jnausb.LibUSB.DeviceHandle;

import java.awt.*;
import java.io.*;
import java.net.*;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

// TODO: This app needs to be able to override or turn off actual vibe settings
// TODO: Personal or testing mode so you don't need to be on SL to play with it
// TODO: webstart
// TODO: music feed?  -- tie into currently playing audio
// TODO: Logging
// TODO: Save config in user's home directory

public final class RLVibe
    implements Runnable
{
  private static final String NO_DEVICE = "NO_DEVICE";
  private static final String DEVICE_CONNECTED = "DEVICE_CONNECTED";
  private static final int TOUCH_VENDOR = 0x0b49;
  private static final int TOUCH_PRODUCT = 0x064f;
  private static final int PATTERN_NONE = 0;
  private static final int PATTERN_RANDOM = 1;
  private static final int PATTERN_HEARTBEAT = 2;
  private static final int PATTERN_SINEWAVE = 3;
  private static final Logger log = Logger.getLogger(RLVibe.class.getName());
  private static final int FOURTY_PERCENT = 26316;
  private static final int SEVENTYFIVE_PERCENT = 49344;
  private static final int ONEHUNDRED_PERCENT = 65000;
  private static final int MAX_SPEED = 0xFF;
  private static RLVibe instance = null;
  public final String userHomeDir = System.getProperty("user.home");
  public final File settings = new File(
      userHomeDir + Toolbox.fileSeparator + ".rlvibe" + Toolbox.fileSeparator + "rlvibe.properties");
  private final Object sleepLock = new Object();
  private final Properties appProperties = new Properties();
  private GUI gui = null;
  private volatile int currentSpeed = 0;
  private volatile boolean doneRunning = false;
  private LibUSB usb = null;
  private Context usbContext = null;
  private DeviceDescriptor descriptor = null;
  private Device device = null;
  private DeviceHandle handle = null;
  private volatile boolean connected = false;
  private Random rand = new Random(System.currentTimeMillis());
  private int pattern = PATTERN_NONE;
  private Thread thread = null;

  private RLVibe()
      throws
      UnsatisfiedLinkError
  {
    // load properties
    try
    {
      appProperties.load(new FileInputStream(settings));
    }
    catch (FileNotFoundException ignore)
    {
      try
      {
        final boolean mkdirs = settings.getParentFile().mkdirs();
        if (mkdirs)
        {
          if (!settings.createNewFile())
          {
            log.severe("Unable to create settings file!");
          }
        }
        else
          log.severe("Unable to build tree structure for settings file!");
      }
      catch (IOException io)
      {
        io.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
      }

    }
    catch (IOException e)
    {
      e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
    }

    // Start up the webservice
    int port = 6969;
    try
    {
      port = Integer.parseInt(appProperties.getProperty("port", "6969"));
    }
    catch (NumberFormatException e)
    {
      log.log(Level.SEVERE, e.getLocalizedMessage(), e);
    }
    final InetSocketAddress addr = new InetSocketAddress("localhost", port);
    HttpServer server = null;
    try
    {
      server = HttpServer.create(addr, 0);
    }
    catch (IOException e)
    {
      log.log(Level.SEVERE, e.getLocalizedMessage(), e);
    }

    server.createContext("/rlvibe", new MyHandler());
    server.setExecutor(Executors.newCachedThreadPool());
    server.start();

    initialize();
    setSpeed(0);
  }

  public static synchronized void newInstance()
      throws
      UnsatisfiedLinkError
  {
    if (instance == null)
    {
      instance = new RLVibe();
      new Thread(instance).start();
    }
  }

  public static synchronized RLVibe getInstance()
  {
    return instance;
  }

  public static void main(final String... args)
  {
    Runtime.getRuntime().addShutdownHook(new ShutdownHook());
    log.setLevel(Level.ALL);
    log.info("library path: " + System.getProperty("java.library.path")); // NON-NLS
    EventQueue.invokeLater(new Runnable()
    {
      public void run()
      {
        new GUI().setVisible(true);
      }
    });
  }

  /**
   * A helper method to send data to a remote webservice.
   *
   * @param url The URL to send to.
   * @return The body of the response
   * @throws IOException Thrown if there is a problem with the socket communication.
   */
  private String sendHttpRequest(final URL url)
      throws
      IOException
  {
    BufferedReader in = null;
    final StringBuilder response = new StringBuilder(1000);
    try
    {
      final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
      conn.setDoInput(true);
      conn.setRequestMethod("GET");
      conn.connect();
      gui.setStatusMessage(Literals.CONNECTED);
      in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
      String str;
      while ((str = in.readLine()) != null)
        response.append(str);
    }
    catch (ConnectException connEx)
    {
      gui.setStatusMessageRaw(connEx.getLocalizedMessage());
    }
    catch (FileNotFoundException fnf)
    {
      if (fnf.getMessage().equals(url.toString()))
      {
        stop();
        gui.setStatusMessage(Literals.DISCONNECTED);
      }
      else
        log.severe(url.toString() + " doesn't match");
    }
    finally
    {
      if (in != null)
        try
        {
          in.close();
        }
        catch (IOException ignore)
        {
        }
    }
    return response.toString();
  }

  private synchronized void initialize()
  {
    if (usbContext == null)
      usbContext = LibUSB.Util.initialize();
    if (usb == null)
      usb = LibUSB.Util.libUSB;
    openDevice();
  }

  private void connect(final URL rlvibeHost)
      throws
      IOException
  {
    log.info("Starting connection thread " + Thread.currentThread().getName());
    gui.setStatusMessage("CONNECTING");
    doneRunning = false;
    while (!doneRunning)
    {
      final String value = sendHttpRequest(rlvibeHost);
      if (Thread.currentThread().isInterrupted())
      {
        log.info("Stopping connection thread " + Thread.currentThread().getName());
        return;
      }
      try
      {
        setSpeed(Integer.parseInt(value));
        pattern = PATTERN_NONE;
      }
      catch (NumberFormatException ignore)
      {
        if (value.toLowerCase().equals("r"))
          pattern = PATTERN_RANDOM;
        if (value.toLowerCase().equals("h"))
          pattern = PATTERN_HEARTBEAT;
        if (value.toLowerCase().equals("s"))
          pattern = PATTERN_SINEWAVE;
      }
      switch (pattern)
      {
        case PATTERN_HEARTBEAT: gui.setTitleText("Heart Beat"); break;
        case PATTERN_SINEWAVE: gui.setTitleText("Sine Wave"); break;
        case PATTERN_RANDOM: gui.setTitleText("Random"); break;
        default: gui.setTitleText(String.valueOf((int)((float)currentSpeed / 2.55f)) + '%'); break;
      }
    }
  }

  public synchronized void openDevice()
  {
    if (descriptor == null || device == null)
    {
      final Device[] deviceList = LibUSB.Util.getDeviceList(usbContext);
      for (final Device device : deviceList)
      {
        final DeviceDescriptor descriptor = LibUSB.Util.getDeviceDescriptor(device);
        if (descriptor.idVendor == TOUCH_VENDOR && descriptor.idProduct == TOUCH_PRODUCT)
        {
          this.descriptor = descriptor;
          this.device = device;
          break;
        }
      }
    }

    if (descriptor == null)
    {
      handle = null;
      device = null;
      if (gui != null)
        gui.setStatusMessage(NO_DEVICE);
      return;
    }

    handle = LibUSB.Util.getDeviceHandle(device);
    if (handle != null)
      connected = true;

    if (gui != null)
      if (connected)
        gui.setStatusMessage(DEVICE_CONNECTED);
      else
        gui.setStatusMessage(NO_DEVICE);

  }

  public void setSpeed(final int speed)
  {
    if (speed < 0)
      currentSpeed = 0;
    if (speed <= MAX_SPEED)
      currentSpeed = speed;
    else
      currentSpeed = MAX_SPEED;
    log.info("Set current speed to " + currentSpeed);
  }

  public synchronized void closeDevice()
  {
    setSpeed(0);
    connected = false;
    if (handle != null)
    {
      usb.libusb_close(handle);
      handle = null;
    }
    device = null;
    descriptor = null;
    if (usbContext != null)
    {
      usb.libusb_exit(usbContext);
      usbContext = null;
//      usb = null;
    }
  }

  public void setGUI(final GUI gui)
  {
    this.gui = gui;
  }

  public void stop()
  {
    log.info("Marking thread ready to stop.");
    doneRunning = true;
  }


  @Override
  public void run()
  {
    log.info("Starting main thread...");
    int rate = 0;
    int count = 0;
    boolean bAscend = false;
    while (!doneRunning)
    {
      switch (pattern)
      {
        case PATTERN_RANDOM:
        {
          final int value = rand.nextInt(256);
          rate = value + (value << 8);
          break;
        }
        case PATTERN_HEARTBEAT:
          count++;
          if (count < 5 || count > 6 && count < 9)
            rate = FOURTY_PERCENT;
          else if (count > 4 && count < 7)
            rate = SEVENTYFIVE_PERCENT;
          else if (count > 8 && count < 10)
            rate = ONEHUNDRED_PERCENT;
          else
            count = 0;
          break;
        case PATTERN_SINEWAVE:
          if (bAscend)
          {
            if (++count > 100)
            {
              bAscend = false;
              count = 99;
            }
          }
          else
          {
            if (--count < 40)
            {
              bAscend = true;
              count = 40;
            }
          }
          final int value = MAX_SPEED / 100 * count;   // TODO: watch this!!  It used to be a float for some reason
          rate = value + (value << 8);
          break;
        case PATTERN_NONE:
        default:
          rate = currentSpeed + (currentSpeed << 8);
      }
      if (!connected)
        initialize();

      synchronized (sleepLock)
      {
        try
        {
          if (connected && handle != null)
          {
            final int ret = usb.libusb_control_transfer(handle, (short) 0x41, (short) 0x00, rate, 0, "", 0, 1000);
            switch (ret)
            {
              case LibUSB.LIBUSB_ERROR_NOT_FOUND: // appears to occur when the controller is unplugged
              case LibUSB.LIBUSB_ERROR_NO_DEVICE: // appears to occur when controller unplugged on Mac
                closeDevice();
                break;
              case LibUSB.LIBUSB_ERROR_PIPE: // appears to be a normal condition with this device
                break;
              default:
                log.warning("Never before seen return code: " + ret);
            }
          }
          Thread.sleep(100);
        }
        catch (InterruptedException ignore)
        {
        }
      }
    }
    setSpeed(0);
    gui.setTitleText("0%");
  }

  @Override
  public synchronized String toString()
  {
    return "RLVibe{" +
           "userHomeDir='" + userHomeDir + '\'' +
           ", settings=" + settings +
           ", sleepLock=" + sleepLock +
           ", appProperties=" + appProperties +
           ", gui=" + gui +
           ", currentSpeed=" + currentSpeed +
           ", doneRunning=" + doneRunning +
           ", usb=" + usb +
           ", usbContext=" + usbContext +
           ", descriptor=" + descriptor +
           ", device=" + device +
           ", handle=" + handle +
           ", connected=" + connected +
           ", rand=" + rand +
           ", pattern=" + pattern +
           ", thread=" + thread +
           '}';
  }

  private class MyHandler
      implements HttpHandler
  {
    @Override
    public void handle(final HttpExchange exchange)
        throws
        IOException
    {
      final String requestMethod = exchange.getRequestMethod();
      if (requestMethod.equalsIgnoreCase("GET"))
      {
        final Headers responseHeaders = exchange.getResponseHeaders();
        final URI uri = exchange.getRequestURI();
        responseHeaders.set("Content-Type", "text/html");
        exchange.sendResponseHeaders(200, 0);

        final OutputStream responseBody = exchange.getResponseBody();
        final String path = uri.getPath();
        if (path.equals("/rlvibe"))
        {
          final String query = uri.getQuery();
          final String slObject = query.substring(query.indexOf('=') + 1);

          try
          {
            final URL rlvibeHost = new URL(slObject);
            if (thread != null)
              thread.interrupt();
            thread = new Thread()
            {
              public void run()
              {
                try
                {
                  connect(rlvibeHost);
                }
                catch (IOException e)
                {
                  e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                }
              }
            };
            thread.start();
          }
          catch (MalformedURLException e)
          {
            final String msg = "Invalid URL: " + slObject;

            gui.setStatusMessage("INVALID_URL", new Object[] { slObject });
            log.log(Level.SEVERE, msg, e);
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
          }
        }

        responseBody.write(("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n" +
                           "<html>\n" +
                           "<head/>\n" +
                            "<body><a href=\"javascript:window.opener='x';window.close();\">Close</a></body></html>\n").getBytes());
        responseBody.close();
      }
    }
  }
}
